Skip to content

35. Search Insert Position#45

Open
Ryotaro25 wants to merge 6 commits intomainfrom
problem41
Open

35. Search Insert Position#45
Ryotaro25 wants to merge 6 commits intomainfrom
problem41

Conversation

@Ryotaro25
Copy link
Copy Markdown
Owner

問題へのリンク
https://leetcode.com/problems/search-insert-position/description/

問題文(プレミアムの場合)

備考

次に解く問題の予告
Find Minimum in Rotated Sorted Array

フォルダ構成
LeetCodeの問題ごとにフォルダを作成します。
フォルダ内は、step1.cpp、step2.cpp、step3.cpp、for.cppとwhile.cppとmemo.mdとなります。

memo.md内に各ステップで感じたことを追記します。

@@ -0,0 +1,63 @@
## ステップ1
制約にO(log n)とあったので思いついたのは、バイナリーサーチツリーを用いた
探索した結果端に辿り着いた場合の処理に時間がかかった
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

二分探索についてはDiscord内で沢山議論がされているので、探してみると良さそうです。

自分が役に立ったなと思うのはこの辺
Yoshiki-Iwasa/Arai60#35 (comment)
Yoshiki-Iwasa/Arai60#35 (comment)

https://discord.com/channels/1084280443945353267/1084283898617417748/1282392271643345007
(上記のリンク先でOdaさんやnodchipさんが話してる内容)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seal-azarashi/leetcode#39 (comment)
こういう風にコードを変えたときにどこは動いてどこは動かないかを分かっていれば、正しく頭の中でモデルが作れていると思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Yoshiki-Iwasa @oda
資料ありがとうございます。同ジャンルの問題を解いてみてこの辺よくわかっていないと気づいたのでじっくり落とし込んでみます。

}

private:
int SearchInsertIndex(int start, int end, vector<int>& nums, int target) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これ、start と end はどういう変数であるかを意識していたらいいと思います。
「nums の start よりも左の要素はすべて target 未満」ということでしょうか。

Copy link
Copy Markdown
Owner Author

@Ryotaro25 Ryotaro25 Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oda

start と end はどういう変数であるかを意識していたらいいと思います。

意識できていなかったです。
startが挿入位置になるように意識したものを追加しました。
049a0f6

return middle;
}
if (nums[middle] < target) {
start++;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こうすると、target が大きい場合、start が一つずつ増えていくので、全部舐めることになりますね。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oda
binary searchになっておりませんでした。他の数問といて戻ってくるとおかしいですね。
start = middle + 1としました。

if (start > end) {
return start;
}
int middle = (start + end) / 2;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

今回の問題の制約では起こりえないのですが、値が大きい場合のオーバーフローを避けるため、
int middle = start + (end - start) / 2;
と書くことをお勧めいたします。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leetcodeにも開設ございました。
ありがとうございます。

@@ -0,0 +1,63 @@
## ステップ1
制約にO(log n)とあったので思いついたのは、バイナリーサーチツリーを用いた
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

回答ソースコードで使用しているのはバイナリーサーチツリーではなくバイナリーサーチではないでしょうか?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

訂正しました🙇‍♂️

private:
int SearchInsertIndex(int start, int end, vector<int>& nums, int target) {
// 端にいってしまった場合の処理
if (start > end) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

区間を閉区間としてとらえているため、 start > end となることはないと思います。終了条件は start == end となると思います。

Copy link
Copy Markdown
Owner Author

@Ryotaro25 Ryotaro25 Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nodchip
レビューありがとうございます。
下記で頂いたコメントを元にstep4.cppを追加しました。

#47

コードの中に、フレームワークを用いてコメントを追記しました。

start > end ではダメな理由としては、以下のように理解で合っておりますでしょうか?
まずstart〜endの中に挿入位置が存在すること=閉区間
であるので、start > endを終了条件にすると、探索の範囲が[2, 1]であったり[0, -1]のような状態が発生しstart~endの中に挿入位置が存在することと矛盾するため。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.問題のモデル化

の部分で、

  • 一番左端の true のいちを求める、もしくは false と true の問題とみなす。
  • 上記問題を解くにあたり、対象の位置を含む区間を定義する。
    が抜けているように思いました。

4.ループ不変条件の設定

一番書くべきことは

  • ループの不変条件を start < end とする。
    だと思います。

5.探索ロジックの設計
nums[mid] < targetがtrueなら、startをmiddle + 1に更新

この書き方ですと、 start を mid に設定してはいけない理由が分かりませんでした。
nums[mid] < target より、探したい対象が mid より右にあることが分かります。区間は閉区間で、区間の中に対象が含まれるので、 start は mid より右に設定してあげるのがよいはずです。そのため、 start を mid + 1 に設定します。
また、 start を mid に設定した場合、区間内の要素数が残り 2 個になったときに、無限ループとなります。

nums[mid] >= targetがtrueなら、endをmiddleに更新
endをmiddle - 1に更新してしまうと、start > endが発生しうる

最終的な挙動からボトムアップに思考しているように見え、違和感を感じました。二分探索の問題のモデルからトップダウンに考えていき、その内容を記述するのが良いと思います。
nums[mid] >= target の場合、 mid の位置に対象がある場合があるため、区間を狭めつつ mid を区間内に含めるため、 end = mid とすると考えるのが良いと思います。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nodchip
詳細に説明いただきありがとうございます。

一番書くべきことは
ループの不変条件を start < end とする。

ここですが start <= middle < end のような書き方でもよろしいでしょうか?
startとendの関係は、start < end変わらないとは思いますが、middleがある方が理解しやすいと思いました。

最終的な挙動からボトムアップに思考しているように見え、違和感を感じました。二分探索の問題のモデルからトップダウンに考えていき、その内容を記述するのが良いと思います。

指摘されるまで気づかなかったのですが、確かに答えから説明を作っていました。

頂いた指摘事項を元にstep4の説明を修正しました。また練習として半開区間を用いたstep5.cppを追加しました🙇‍♂️
お手隙の際に見ていただけると幸いです。
f1f955b

Copy link
Copy Markdown

@nodchip nodchip Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここですが start <= middle < end のような書き方でもよろしいでしょうか?

閉区間で考えているため、 middle == end の場合もあります。そのため、 middle < end とはかけないと思います。 start < end、start <= middle、middle <= end と書くしかないように思います。

7.実行
leet codeにて動作確認

*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なんとなく理解しているか不安を感じています。

「この関数の仕事を手作業でやっているとしましょう。シフト制で SearchInsertIndex の呼び出しが起きるごとに、人が交代します。

あなたは、当番で SearchInsertIndex の呼び出しがおきたという連絡を受けて、仕事につきます。
start, end, nums, target が与えられました。

ここまで働いている人たちがきちんと仕事をしていたら、start, end, nums, target についてどのようなことがいえますか。」

という質問に答えられますか。

まず、自分が呼び出した前任者がしていた仕事は3通りの可能性がありますね。

SearchInsertIndex(0, nums.size() - 1, nums, target);
SearchInsertIndex(middle + 1, end, nums, target);
SearchInsertIndex(start, middle, nums, target);

前任者たちが正しく仕事をしていたら、どういう条件のものが送られてきますか。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oda

なんとなく理解しているか不安を感じています。

紙に書いて処理を追ったり、なんとか言葉に落とし込んだりしているもののまだスッキリしておりません。
手作業だと下記のような感じでしょうか。

1番最初に作業をする人からは、確認作業の範囲と確認対象とターゲットの数字が引き継がれます。
SearchInsertIndex(0, nums.size() - 1, nums, target);
作業は始まったところであるため、範囲はnumsの先頭から1番最後まで取ります。

2番目作業者からは効率よく探索を行うためnums内のおおよそどこにターゲットがあるのかあたり付けをします。
startとendの情報を元に、numsの真ん中の数字を確認します。

この時にターゲットが真ん中より大きい場合は、下記の形で呼び出します。
SearchInsertIndex(middle + 1, end, nums, target);
次の作業者へ真ん中の数字より大きいためstart 〜 middle内にはターゲットが存在しないことを伝えます。
次の作業者の作業範囲はmiddleの次の位置から、numsの最後となります。

ターゲットが真ん中の数字以下の場合(真ん中より大きくない場合)は下記の形で呼び出します。
SearchInsertIndex(start, middle, nums, target);
これはstartからmiddleを含むどこかにターゲットが存在することを知らせております。
numsの始まりから真ん中を含む範囲を確認するよう依頼します。

3番目以降の作業者も効率よく作業をするため、前任から引き継がれた作業範囲を元に
真ん中を見つけて次の人へ作業範囲を分割して引き継ぎます。

これを作業範囲がなくなるまで繰り返します。

start, end, nums, target についてどのようなことがいえますか。

・startはtargetの位置に対して常に左側(startの位置とtargetの位置がイコールの場合もある)
・endはtargetの位置に対して常に右側
・前任者から引き継いだstartとendは、自分がmiddle分割したmiddle含むまでの範囲とmiddleの次以降から最後までを合わせたものと一致します。
2番目の作業者が分割したstart 〜 middleとmiddleの次 〜 end くっつけたものは、1番最初に作業をした人が調べたstart 〜 endと同じになります。
・nums, target は常に不変です。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。そうですね。正しい方向に考えていると思います。

endはtargetの位置に対して常に右側

これなんですが、一番はじめのループのときだけ、これが成立していませんよね。
end = nums.size() - 1 としているので、同じ可能性があります。

これが最後に if で分岐をする羽目になった理由です。

Copy link
Copy Markdown
Owner Author

@Ryotaro25 Ryotaro25 Dec 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oda

これなんですが、一番はじめのループのときだけ、これが成立していませんよね。
end = nums.size() - 1 としているので、同じ可能性があります。

この部分に気づけませんでした。
小田さんと野田さんから頂いたコメントを元にstep4の説明を修正しました。
また練習として半開区間を用いたstep5.cppを追加しました🙇‍♂️
f1f955b

@Ryotaro25
Copy link
Copy Markdown
Owner Author

step4に修正反映もれございました🙇‍♂️
25ac1e8

@@ -0,0 +1,61 @@
/*
1.問題のモデル化
ターゲットより大きいのか、以下なのか2つ以上の相補的な状態に分類することができるのでfalseとtrueの問題とみなす。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

falseとtrueの問題とみなす。

この部分の意味が理解できませんでした。この部分を文字通り読むと、 true または false を返す問題のように思えます。元の問題は、nums の中に target が存在していればその位置を、なければ挿入位置を返す問題です。

また、二分探索の問題としてとらえるにあたり、 [false, false, ..., false, true, true, ..., true] といった配列の一番左の true の位置、または false と true の境界の位置を探す、という記述が必要だと思います。

仮に自分が書くとするならば、以下のように書くと思います。

nums の各要素について、 target 未満を false、 target 以上を true とするような配列を作る。この配列は [false, false, ..., false, true, true, ..., true] のように、 0 個以上の false が並び、そのあと 0 個以上の true が並ぶ。この配列の配列の中で、一番左の true の位置を探す。

今回は閉区間として探索を行う。

3.初期値の設定
startを0、endを配列の最後の要素nums.size() - 1として探索する。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この書き方ですと、初期値の設定のフェーズにもかかわらず、探索を始めてしまっているように見えます。

startを0、endを配列の最後の要素nums.size() - 1とする。

でよいと思います。


3.初期値の設定
startを0、endを配列の最後の要素nums.size() - 1として探索する。
配列の全要素を探索するイメージ。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

こちらも、初期値の設定のフェーズにもかかわらず、探索を始めてしまっているように見えます。さらに、この書き方ですと、全要素を探索すると時間がかかるので、一部のみ調べて、処理時間を節約するという点がポイントにもかかわらず、全要素を調べようとしているかのように感じられます。

配列の全要素を探索するイメージ。

4.ループ不変条件の設定
startとendの真ん中をmiddleとして、ループの普遍条件は
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 不変

・start <= middle < end

5.探索ロジックの設計
nums[middle] < targetがtrueであれば、middleより左側にtargetは存在しないので
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

middle の位置にも target は存在していないので、この記述ですと誤りだと思います。また、「nums[middle] < targetがtrueであれば」表現が重複しているように思います。「nums[middle] < target の場合」でよいと思います。

nums[middle] < target の場合、middleおよびその左側にtargetは存在しないので


5.探索ロジックの設計
nums[middle] < targetがtrueであれば、middleより左側にtargetは存在しないので
startをmiddle + 1に更新
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

読み手の認知負荷を下げるため、同一文章の中で、体言止めと用言止めは統一することをお勧めいたします。

start を middle + 1 に更新する。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nodchip
たくさんレビューを頂いていたにも関わらず放置となってしまい申し訳ございません。

本問題含め色々と教えていただいておりましたが理解が進まなかったため、Leet Codeの学習コースでBinary searchまで勉強してきました。

頂いておりましたレビューを見直してからstep4のモデル化に関するコメントを修正しました。
33fef01

またこの問題を改めて解きました。
その際に最もしっくりきた回答をwhile_step3.cppに追加しました。モデル化に関するコメントもこちらに記載しております。
33fef01

@Ryotaro25
Copy link
Copy Markdown
Owner Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants